package com.hooda.framework.factory;

import com.hooda.framework.annotations.Autowired;
import com.hooda.framework.annotations.Component;
import com.hooda.framework.annotations.Repository;
import com.hooda.framework.annotations.Service;
import com.hooda.utils.ProxyUtils;
import org.apache.commons.lang3.tuple.Triple;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @author zhangzp
 * @version 1.0, 2021/8/12
 */
public final class BeanFactory {

    private Properties properties = new Properties();

    private List<String> classNames = new ArrayList<>();

    private Map<String, Object> beans = new HashMap<>();

    private Map<String, Object> proxyBeans = new HashMap<>();

    private List<MethodInterceptor> interceptors = new ArrayList<>();

    private static BeanFactory instance = new BeanFactory();

    public static BeanFactory getInstance() {
        return instance;
    }

    private BeanFactory() {
        init();
    }

    private void init() {
        try {
            doLoadProperties("config.properties");

            doScan(properties.getProperty("scan.package"));

            doInstance();

            doAutowired();

            doProxy();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加载配置文件
     * @param configPath 配置路径
     * @throws IOException
     */
    private void doLoadProperties(String configPath) throws IOException {
        InputStream input = BeanFactory.class.getClassLoader().getResourceAsStream(configPath);
        properties.load(input);
    }

    /**
     * 扫描.class文件
     * @param scanPackage
     */
    private void doScan(String scanPackage) {
        String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() + scanPackage.replaceAll("\\.", "/");
        File pack = new File(scanPackagePath);

        File[] files = pack.listFiles();

        for (File file : files) {
            if (file.isDirectory()) {
                doScan(scanPackage + "." + file.getName());
            } else if (file.getName().endsWith(".class")) {
                String className = scanPackage + "." + file.getName().replaceAll(".class", "");
                classNames.add(className);
            }
        }
    }

    /**
     * 实例化注解类
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private void doInstance() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        for (int i = 0; i < classNames.size(); i++) {
            String clazz = classNames.get(i);
            Class<?> aClass = Class.forName(clazz);
            String beanName = "";
            if (!aClass.isAnnotationPresent(Service.class) && !aClass.isAnnotationPresent(Repository.class) && !aClass.isAnnotationPresent(Component.class)) {
                continue;
            }
            if (aClass.isAnnotationPresent(Service.class)) {
                Service annotation = aClass.getAnnotation(Service.class);
                beanName = annotation.value();
            } else if (aClass.isAnnotationPresent(Repository.class)) {
                Repository annotation = aClass.getAnnotation(Repository.class);
                beanName = annotation.value();
            } else if (aClass.isAnnotationPresent(Component.class)) {
                Component annotation = aClass.getAnnotation(Component.class);
                beanName = annotation.value();
            }
            if ("".equals(beanName)) {
                beanName = lowerFirst(aClass.getSimpleName());
            }
            beans.put(beanName, aClass.newInstance());
        }
    }

    /**
     * 注入属性和方法
     * @throws IllegalAccessException
     */
    private void doAutowired() throws IllegalAccessException, InvocationTargetException {
        for (Map.Entry<String, Object> entry : beans.entrySet()) {

            Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
            for (int i = 0; i < declaredFields.length; i++) {
                Field declaredField = declaredFields[i];
                if (!declaredField.isAnnotationPresent(Autowired.class)) {
                    continue;
                }

                Autowired annotation = declaredField.getAnnotation(Autowired.class);
                String beanName = annotation.value();
                if ("".equals(beanName)) {
                    beanName = declaredField.getName();
                }
                declaredField.setAccessible(true);
                declaredField.set(entry.getValue(), beans.get(beanName));
            }

            Method[] declaredMethods = entry.getValue().getClass().getDeclaredMethods();
            for (int i = 0; i < declaredMethods.length; i++) {
                Method declaredMethod = declaredMethods[i];
                if (!declaredMethod.isAnnotationPresent(Autowired.class)) {
                    continue;
                }

                if (!declaredMethod.getName().startsWith("set")) {
                    continue;
                }

                if (declaredMethod.getParameterCount() != 1) {
                    throw new UnsupportedOperationException(declaredMethod.getName());
                }

                Autowired annotation = declaredMethod.getAnnotation(Autowired.class);
                String valueName = annotation.value();
                if ("".equals(valueName)) {
                    valueName = lowerFirst(declaredMethod.getName().substring(3));
                }
                Object value;
                if ("java.lang.String".equals(declaredMethod.getParameterTypes()[0].getName())) {
                    value = properties.getProperty(valueName);
                } else {
                    value = beans.get(valueName);
                }
                declaredMethod.setAccessible(true);
                declaredMethod.invoke(entry.getValue(), value);
            }
        }
    }

    /**
     * 代理类
     */
    private void doProxy() {
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            if (entry.getValue() instanceof MethodInterceptor) {
                interceptors.add((MethodInterceptor) entry.getValue());
                continue;
            }
            Object bean = ProxyUtils.createCglibProxy(entry.getValue(), this::handle);
            // TODO：因为目前事务的处理方式在JDK动态代理下暂时无法实现，所以暂时使用Cglib代理
//            Class<?> aClass = entry.getValue().getClass();
//            if (aClass.getInterfaces() == null || aClass.getInterfaces().length <= 0) {
//                bean = ProxyUtils.createCglibProxy(entry.getValue(), this::handle);
//            } else {
//                bean = ProxyUtils.createJdkProxy(entry.getValue(), this::handle);
//            }
            proxyBeans.put(entry.getKey(), bean);
        }
    }

    /**
     * 代理回调
     * @param target
     * @param method
     * @param args
     * @return
     * @throws Exception
     */
    private Object handle(Object target, Method method, Object[] args) throws Exception {

        Object result;
        try {
            for (MethodInterceptor interceptor : interceptors) {
                interceptor.before(target, method, args);
            }

            result = method.invoke(target, args);

            for (MethodInterceptor interceptor : interceptors) {
                interceptor.afterReturnning(target, method, args);
            }

            return result;
        } catch (Exception e) {
            for (MethodInterceptor interceptor : interceptors) {
                interceptor.afterThrowing(target, method, args);
            }

            throw e;
        } finally {
            for (MethodInterceptor interceptor : interceptors) {
                interceptor.after(target, method, args);
            }
        }
    }

    public static String lowerFirst(String str) {
        char[] chars = str.toCharArray();
        if ('A' <= chars[0] && chars[0] <= 'Z') {
            chars[0] += 32;
        }
        return String.valueOf(chars);
    }

    public Object getBean(String id) {
        return proxyBeans.get(id);
    }
}
